Skip to content

Conversation

hi-ogawa
Copy link
Contributor

@hi-ogawa hi-ogawa commented Aug 27, 2025

Description

Reproduction

  • increment counter "test-hmr-client-dep2"
  • edit hmr-client-dep2/client-dep.ts
  • click "re-render"
  • counter is reset to 0

This is because when client-dep.ts is modified, it invalidates client.tsx (on client environment) for hmr, which becomes a new module fetched as client.tsx?t=xxx, but client.tsx on server environment (which exists as "proxy module" with registerClientReference) is not invalidated. When re-rendering RSC, server payload includes a client component with old module reference client.tsx instead of new client.tsx?t=xxx, which has a different identity as TestHmrClientDep2 functional component and thus React re-mounts node.

Screencast.From.2025-08-27.16-06-40.mp4

TODO

  • test
  • fix
  • compare with next.js and parcel
    • they use module cache with consistent "module id" in memory (not browser's esm module cache), so they probably don't have an equivalent issue.
  • propose official API on vite instead patching internal
    • ideally we should coordinate moduleGraph.invalidateModule from client environment to rsc environment.

@hi-ogawa hi-ogawa force-pushed the 08-27-fix_rsc_propagate_client_reference_invalidation_to_server branch from e010a1f to da11904 Compare August 27, 2025 07:40
@hi-ogawa hi-ogawa added the trigger: preview Trigger pkg.pr.new label Aug 27, 2025
Copy link

pkg-pr-new bot commented Aug 27, 2025

Open in StackBlitz

npm i https://pkg.pr.new/@vitejs/plugin-react@788
npm i https://pkg.pr.new/@vitejs/plugin-react-oxc@788
npm i https://pkg.pr.new/@vitejs/plugin-rsc@788
npm i https://pkg.pr.new/@vitejs/plugin-react-swc@788

commit: 5058fa3

Comment on lines +418 to +445
// intercept client hmr to propagate client boundary invalidation to server environment
const oldSend = server.environments.client.hot.send
server.environments.client.hot.send = async function (
this,
...args: any[]
) {
const e = args[0] as vite.UpdatePayload
if (e && typeof e === 'object' && e.type === 'update') {
for (const update of e.updates) {
if (update.type === 'js-update') {
const mod =
server.environments.client.moduleGraph.urlToModuleMap.get(
update.path,
)
if (mod && mod.id && manager.clientReferenceMetaMap[mod.id]) {
const serverMod =
server.environments.rsc!.moduleGraph.getModuleById(mod.id)
if (serverMod) {
server.environments.rsc!.moduleGraph.invalidateModule(
serverMod,
)
}
}
}
}
}
return oldSend.apply(this, args as any)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this only covers the case where client boundary gets hmr-ed, technically there are edge cases like client reference itself doesn't self-accept or invalidateModule is directly used, so they causes full-reload instead.

Copy link
Contributor Author

@hi-ogawa hi-ogawa Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, patching invalidateModule directly will break the first test case "non-client-reference client hmr" as this is more aggressive invalidation.

const oldInvalidateModule = server.environments.client.moduleGraph.invalidateModule
server.environments.client.moduleGraph.invalidateModule = function (this, ...args) {
  const mod = args[0];
  if (mod && mod.id && manager.clientReferenceMetaMap[mod.id]) {
    const serverMod =
      server.environments.rsc!.moduleGraph.getModuleById(mod.id)
    if (serverMod) {
      server.environments.rsc!.moduleGraph.invalidateModule(
        serverMod,
      )
    }
  }
  return oldInvalidateModule.apply(this, args);
}

@hi-ogawa hi-ogawa marked this pull request as ready for review August 27, 2025 09:12
@hi-ogawa
Copy link
Contributor Author

hi-ogawa commented Aug 27, 2025

I think this still has an issue when "use client" module is imported both from server component and from other "use client" module. In that case, the scenario like "non-client-reference client hmr" test case will end up loading two version since the one imported from server component doesn't have invalidated timestamp.

server1       server2
   |             |
   v             v
client1 <--- client2
   |
   v
dep-comp

Here changing dep-comp would load only dep-comp?t=....

However, upon refresh, it will probably end up with:

server1                    server2                  
   |                         |
   v                         v
client1   client1?t=xx <-- client2
   |         |                               
   v         v                      
  dep-comp?t=xx

@hi-ogawa hi-ogawa merged commit a8dc3fe into main Aug 28, 2025
21 checks passed
@hi-ogawa hi-ogawa deleted the 08-27-fix_rsc_propagate_client_reference_invalidation_to_server branch August 28, 2025 02:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

trigger: preview Trigger pkg.pr.new

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant